Work with compiler------premain启动优化

Mach-O

在Xcode中构建的程序,经由预处理、编译、汇编、链接将源文件(.m和.h文件)转变为Mach-O 二进制可执行文件。Mach-O 中代码被划分为不同的Segment,而每个Segment又包含多个Section。

Mach-O的架构:

Segment

执行一个可执行文件时。虚拟内存系统会将Segment映射到进程的地址空间。在虚拟内存系统进行映射时,不同的Segment会以不同的参数(权限)被映射。常见的Segment有:

  • __TEXT :__TEXT段包含了可执行的代码。它们被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。
  • __DATA:程序数据段以可读写但不可执行的方式映射。(本文讨论的重点)。
  • __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用。
  • __LINKEDIT: 链接器使用的符号以及其他表。

Section

Section 是具体有用的数据存放的地方,下表列出了__DATA 和 __TEXT下的Section:


__attribute__

__attribute__是一种编译器指令,可以让编译器做更多地错误检查和代码优化,其中就包括大家常见的废弃掉方法、类或者变量的指令__attribute__ ((deprecated(“”)))。

__attribute__的一般语法形式是__attribute__关键字后面跟两个小括号,在小括号里是逗号分隔的选项。这里我们重点关注section() 函数

section() 函数

前面提到__DATA段为可读写程序数据段,而section() 函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。在阅读runtime源码的过程中,可以发现苹果大量利用__attribute__和section()函数在编译器将数据写入__DATA段。

比如,下述代码编译器在__DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组,用于运行期category的加载:

1
2
3
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_MyClass_$_MyAddition,
};

同样,我们也可以利用__attribute__和section()将数据提前写入__DATA段,以达到程序优化的目的(比如,去+load)。+load()提供了一个非常靠前的执行时机,在实际开发中很多基础库的初始化工作都集中在这个时机执行。而过重的+load()会拖慢程序的premain的加载时间,有了上述的理论基础我们可以在将相关初始化工作封装在函数内并在编译期将相关函数指针写入__DATA段,从而达到延迟加载的效果,节省启动时间。

Coding

受够了理论基础,是时候敲一段代码了~ ,以下代码我们在__DATA段新增section __bwkcfunction__,并将func()的函数指针写入__bwkcfunction__中。

1
2
3
4
5
6
7
8
9
typedef void (* BWKCFunction) (void);

void func (void){
printf("hello world in c fun");
}

void main (){
__attribute__((used,section("__DATA"",""__bwkcfunction__"))) static const BWKCFunction cfunc = func;
}

接着,我们通过命令行编译该代码生成可执行文件a.out(a.out 是clang的默认命名)。

1
xcrun clang test_attribute.c

最后通过命令行看一下我们的成果吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ xcrun size -x -l -m a.out 

Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x26 (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f76 offset 3958)
Section __stub_helper: 0x1a (addr 0x100000f7c offset 3964)
Section __cstring: 0x16 (addr 0x100000f96 offset 3990)
Section __unwind_info: 0x48 (addr 0x100000fac offset 4012)
total 0xa4
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
Section __bwkcfunction__: 0x8 (addr 0x100001018 offset 4120)
total 0x20
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

可以看到,在_DATA段,我们成功插入了\_bwkcfunction__ section,且该section的大小为8字节(一个c语言指针的大小)。然后我们就可以在程序运行的过程中在需要的时候取出对应的函数指针,从而实现懒加载~